package com.imooc.service.impl;


import com.imooc.dataobject.ProductInfo;
import com.imooc.service.ProductInfoService;
import com.imooc.utils.KeyUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;

/**
 * 验证一下事务失效场景的类。
 * 资料来源： https://mp.weixin.qq.com/s/4M4rePjjy8-UBVvCzHchTQ     (mp是公众号的意思)
 */
@Service
@Slf4j
public class ValidateTransactionService {

    @Autowired
    private ProductInfoService productInfoService;

    /**
     * 1、测试下非public场景：事务生效，回滚了
     */
    @Transactional(rollbackFor = Throwable.class)
    public void testPublicMethod(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        int i = 1/0;
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
    }

    /**
     * 2、测试下非public场景： 事务不生效
     */
    @Transactional(rollbackFor = Throwable.class)
    protected void testNonPublicMethod(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        int i = 1/0;
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
    }

    /**
     * 3、测试 final方法的场景：不行，直接编译就不过。  static方法一样无法通过动态代理，变成事务方法。
     * 一定要理解，你事务的本质是通过aop给你生成代理，你都final，代理类就不能重写你的方法了，那代理还有啥意义？？
     */
    @Transactional(rollbackFor = Throwable.class)
    public /*final*/ void testFinalMethod(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        int i = 1/0;
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
    }

    /**
     * 4、测试 方法内部调用的场景：不行，testInnerMethod2()里抛异常的话，testInnerMethod2()的事务是不生效的，抛了异常数据不会回滚  （这时候关键是看 testInnerMethod2()方法的结果。。 。）
     * 但我就是要方法内部调用，那怎么保证事务呢。可以用 AopContext.currentProxy()获取代理对象
     */
    public void testInnerMethod1(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        //testInnerMethod2();
        /**
         * 加了这个的话，testInnerMethod2()里抛异常的话，testInnerMethod2()的事务是生效的，数据是回滚了、但仅仅是回滚2，1还是不会回滚（那肯定啊，1又没有加入事务）
         */
        ((ValidateTransactionService)AopContext.currentProxy()).testInnerMethod2();
    }

    @Transactional(rollbackFor = Throwable.class)
    public void testInnerMethod2(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
        int i  = 1/0;
    }

    /**
     * 5、测试多线程场景：不行 、 这时候1和2都不会回滚
     * 两个方法不在同一个线程中(尽管都加了 @Transactional )，获取到的数据库连接都不一样，从而是两个不同的事务。那当然不会生效。
     *
     * 如果看过spring事务源码的朋友，可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map，key是数据源，value是数据库连接。
     */
    @Transactional(rollbackFor = Throwable.class)
    public void testThreadMethod1(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        new Thread(() -> {
            testThreadMethod2();
        }).start();
    }

    @Transactional( rollbackFor = Throwable.class)
    public void testThreadMethod2(){
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
        int i  = 1/0;
    }


    /**
     * 6、异常被你catch住了，事务一样不生效。 这个就不演示，很直观
     * 如果想要spring事务能够正常回滚，必须抛出它能够处理的异常。如果没有抛异常，则spring认为程序是正常的。
     * 下面这个场景类似
     */


    /**
     * 7、手动抛一个Exception：不生效
     * 因为spring事务，默认情况下只会回滚RuntimeException（运行时异常）和Error（错误），对于普通的Exception（非运行时异常），它不会回滚。  注意error默认也会回滚
     * 怎么解决：
     *      1、最好写成  @Transactional(rollbackFor = Exception.class) ，参数不要空着。
     *      即使rollbackFor有默认值，但阿里巴巴开发者规范中，还是要求开发者重新指定该参数。
     *      为什么？
     *      因为如果使用默认值，一旦程序抛出了Exception，事务不会回滚，这会出现很大的bug。所以，建议一般情况下，将该参数设置成：Exception或Throwable
     */
    @Transactional(rollbackFor = Throwable.class)
    public void test手动抛出ExceptionMethod() throws Exception{
        try {
            productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
            int i  = 1/0;
        }catch (Exception e){
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }


    /**
     * 8、 嵌套事务回滚多了、没有设置回滚保存点、加一下try catch就能设置回滚保存点了
     *
     * 这里懒得测试了，直接看帖子吧
     */


    /**
     * 9、 大事务问题。 这块还挺复杂的，
     * 直接看：（这个帖子讲的很好，多看看）
     *          https://mp.weixin.qq.com/s?__biz=MzUxODkzNTQ3Nw==&mid=2247485262&idx=1&sn=abe19452e4c13876270f329cc6929be7&chksm=f9800194cef78882e5ad4d8eb00b7e3f745a4159aee6afb1858cc16cae599f8889afa330e17b&token=305097496&lang=zh_CN&scene=21#wechat_redirect
     *  怎么解决呢？  引出编程式事务， 粒度控制更细 。  上面说的这么一大堆说的都是声明式事务
     */
    @Autowired
    private TransactionTemplate transactionTemplate;

    //这时候就不要 @Transactional() 了
    public void testBigTransaction() {
        /**
         * 这有2句话，我直想让第二句加入事务，第一句不加。这个代码怎么优化呢？ 如下：
         */
        productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字1").productPrice(new BigDecimal(10.00)).productStock(100).categoryType(1).build());
        //这就精准控制了
        transactionTemplate.execute(status -> {
            productInfoService.save(ProductInfo.builder().productId(KeyUtil.genUniqueKey()).productName("产品名字2").productPrice(new BigDecimal(20.00)).productStock(200).categoryType(2).build());
            return Boolean.TRUE;
        });
    }



}
